[Chaper Seventeen][Previous]
[Art of Assembly][Randall
Hyde]
Art of Assembly: Chaper Seventeen
- 17.5 - Chaining Interrupt Service Routines
- 17.6 - Reentrancy Problems
- 17.7 - The Efficiency of an Interrupt
Driven System
- 17.7.1 - Interrupt Driven I/O vs. Polling
- 17.7.2 - Interrupt Service Time
- 17.7.3 - Interrupt Latency
- 17.7.4 - Prioritized Interrupts
- 17.8 - Debugging ISRs
17.5 Chaining Interrupt Service Routines
Interrupt service routines come in two basic varieties - those that
need exclusive access to an interrupt vector and those that must share an
interrupt vector with several other ISRs. Those in the first category include
error handling ISRs (e.g., divide error or overflow) and certain device
drivers. The serial port is a good example of a device that rarely has more
than one ISR associated with it at any one given time[8].
The timer, real-time clock, and keyboard ISRs generally fall into the latter
category. It is not at all uncommon to find several ISRs in memory sharing
each of these interrupts.
Sharing an interrupt vector is rather easy. All an ISR needs to do to share
an interrupt vector is to save the old interrupt vector when installing
the ISR (something you need to do anyway, so you can restore the interrupt
vector when your code terminates) and then call the original ISR before
or after you do your own ISR processing. If you've saved away the address
of the original ISR in the dseg
double word variable OldIntVect
,
you can call the original ISR with the following code:
; Presumably, DS points at DSEG at this point.
pushf ;Simulate an INT instruction by pushing
call OldIntVect ; the flags and making a far call.
Since OldIntVect
is a dword variable, this code generates a
far call to the routine whose segmented address appears in the OldIntVect
variable. This code does not jump to the location of the OldIntVect
variable.
Many interrupt service routines do not modify the ds
register
to point at a local data segment. In fact, some simple ISRs do not change
any of the segment registers. In such cases it is common to put any necessary
variables (especially the old segment value) directly in the code segment.
If you do this, your code could jump directly to the original ISR rather
than calling it. To do so, you would just use the code:
MyISR proc near
.
.
.
jmp cs:OldIntVect
MyISR endp
OldIntVect dword ?
This code sequence passes along your ISR's flags and return address as the
flag and return address values to the original ISR. This is fine, when the
original ISR executes the iret instruction, it will return directly to the
interrupted code (assuming it doesn't pass control to some other ISR in
the chain).
The OldIntVect
variable must be in the code segment if you
use this technique to transfer control to the original ISR. After all, when
you executing the jmp
instruction above, you must have already
restored the state of the CPU, including the ds
register. Therefore,
you have no idea what segment ds
is pointing at, and it probably
isn't pointing at your local data segment. Indeed, the only segment register
whose value is known to you is cs
, so you must keep the vector
address in your code segment.
The following simple program demonstrates interrupt chaining. This short
program patches into the int 1ch
vector. The ISR counts off
seconds and notifies the main program as each second passes. The main program
prints a short message every second. When 10 seconds have expired, this
program removes the ISR from the interrupt chain and terminates.
; TIMER.ASM
; This program demonstrates how to patch into the int 1Ch timer interrupt
; vector and create an interrupt chain.
.xlist
.286
include stdlib.a
includelib stdlib.lib
.list
dseg segment para public 'data'
; The TIMERISR will update the following two variables.
; It will update the MSEC variable every 55 ms.
; It will update the TIMER variable every second.
MSEC word 0
TIMER word 0
dseg ends
cseg segment para public 'code'
assume cs:cseg, ds:dseg
; The OldInt1C variable must be in the code segment because of the
; way TimerISR transfers control to the next ISR in the int 1Ch chain.
OldInt1C dword ?
; The timer interrupt service routine.
; This guy increment MSEC variable by 55 on every interrupt.
; Since this interrupt gets called every 55 msec (approx) the
; MSEC variable contains the current number of milliseconds.
; When this value exceeds 1000 (one second), the ISR subtracts
; 1000 from the MSEC variable and increments TIMER by one.
TimerISR proc near
push ds
push ax
mov ax, dseg
mov ds, ax
mov ax, MSEC
add ax, 55 ;Interrupt every 55 msec.
cmp ax, 1000
jb SetMSEC
inc Timer ;A second just passed.
sub ax, 1000 ;Adjust MSEC value.
SetMSEC: mov MSEC, ax
pop ax
pop ds
jmp cseg:OldInt1C ;Transfer to original ISR.
TimerISR endp
Main proc
mov ax, dseg
mov ds, ax
meminit
; Begin by patching in the address of our ISR into int 1ch's vector.
; Note that we must turn off the interrupts while actually patching
; the interrupt vector and we must ensure that interrupts are turned
; back on afterwards; hence the cli and sti instructions. These are
; required because a timer interrupt could come along between the two
; instructions that write to the int 1Ch interrupt vector. This would
; be a big mess.
mov ax, 0
mov es, ax
mov ax, es:[1ch*4]
mov word ptr OldInt1C, ax
mov ax, es:[1ch*4 + 2]
mov word ptr OldInt1C+2, ax
cli
mov word ptr es:[1Ch*4], offset TimerISR
mov es:[1Ch*4 + 2], cs
sti
; Okay, the ISR updates the TIMER variable every second.
; Continuously print this value until ten seconds have
; elapsed. Then quit.
mov Timer, 0
TimerLoop: printf
byte "Timer = %d\n",0
dword Timer
cmp Timer, 10
jbe TimerLoop
; Okay, restore the interrupt vector. We need the interrupts off
; here for the same reason as above.
mov ax, 0
mov es, ax
cli
mov ax, word ptr OldInt1C
mov es:[1Ch*4], ax
mov ax, word ptr OldInt1C+2
mov es:[1Ch*4+2], ax
sti
Quit: ExitPgm ;DOS macro to quit program.
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 1024 dup ("stack ")
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg ends
end Main
17.6 Reentrancy Problems
A minor problem develops with developing ISRs, what happens if you enable
interrupts while in an ISR and a second interrupt from the same device comes
along? This would interrupt the ISR and then reenter the ISR from the beginning.
Many applications do not behave properly under these conditions. An application
that can properly handle this situation is said to be reentrant. Code segments
that do not operate properly when reentered are nonreentrant.
Consider the TIMER.ASM program in the previous section. This is an example
of a nonreentrant program. Suppose that while executing the ISR, it is interrupted
at the following point:
TimerISR proc near
push ds
push ax
mov ax, dseg
mov ds, ax
mov ax, MSEC
add ax, 55 ;Interrupt every 55 msec.
cmp ax, 1000
jb SetMSEC
; <<<<< Suppose the interrupt occurs at this point >>>>>
inc Timer ;A second just passed.
sub ax, 1000 ;Adjust MSEC value.
SetMSEC: mov MSEC, ax
pop ax
pop ds
jmp cseg:OldInt1C ;Transfer to original ISR.
TimerISR endp
Suppose that, on the first invocation of the interrupt, MSEC contains 950
and Timer
contains three. If a second interrupt occurs and
the specified point above, ax will contain 1005. So the interrupt suspends
the ISR and reenters it from the beginning. Note that TimerISR
is nice enough to preserve the ax
register containing the value
1005. When the second invocation of TimerISR
executes, it finds
that MSEC
still contains 950 because the first invocation has
yet to update MSEC
. Therefore, it adds 55 to this value, determines
that it exceeds 1000, increments Timer
(it becomes four) and
then stores five into MSEC
. Then it returns (by jumping to
the next ISR in the int 1ch
chain). Eventually, control returns
the first invocation of the TimerISR
routine. At this time
(less than 55 msec after updating Timer
by the second invocation)
the TimerISR
code increments the Timer
variable
again and updates MSEC
to five. The problem with this sequence
is that it has incremented the Timer
variable twice in less
than 55 msec.
Now you might argue that hardware interrupts always clear the interrupt
disable flag so it would not be possible for this interrupt to be reentered.
Furthermore, you might argue that this routine is so short, it would never
take more than 55 msec to get to the noted point in the code above. However,
you are forgetting something: some other timer ISR could be in the system
that calls your code after it is done. That code could take 55 msec and
just happen to turn the interrupts back on, making it perfectly possible
that your code could be reentered.
The code between the mov ax, MSEC
and mov MSEC, ax
instructions above is called a critical region or critical section. A program
must not be reentered while it is executing in a critical region. Note that
having critical regions does not mean that a program is not reentrant. Most
programs, even those that are reentrant, have various critical regions.
The key is to prevent an interrupt that could cause a critical region to
be reentered while in that critical region. The easiest way to prevent such
an occurrence is to turn off the interrupts while executing code in a critical
section. We can easily modify the TimerISR
to do this with
the following code:
TimerISR proc near
push ds
push ax
mov ax, dseg
mov ds, ax
; Beginning of critical section, turn off interrupts.
pushf ;Preserve current I flag state.
cli ;Make sure interrupts are off.
mov ax, MSEC
add ax, 55 ;Interrupt every 55 msec.
cmp ax, 1000
jb SetMSEC
inc Timer ;A second just passed.
sub ax, 1000 ;Adjust MSEC value.
SetMSEC: mov MSEC, ax
; End of critical region, restore the I flag to its former glory.
popf
pop ax
pop ds
jmp cseg:OldInt1C ;Transfer to original ISR.
TimerISR endp
We will return to the problem of reentrancy and critical regions in the
next two chapters of this text.
17.7 The Efficiency of an Interrupt Driven System
Interrupts introduce a considerable amount of complexity to a software
system (see "Debugging ISRs" on
page 1020). One might ask if using interrupts is really worth the trouble.
The answer of course, is yes. Why else would people use interrupts if they
were proven not to be worthwhile? However, interrupts are like many other
nifty things in computer science - they have their place; if you attempt
to use interrupts in an inappropriate fashion they will only make things
worse for you.
The following sections explore the efficiency aspects of using interrupts.
As you will soon discover, an interrupt driven system is usually superior
despite the complexity. However, this is not always the case. For many systems,
alternative methods provide better performance.
17.7.1 Interrupt Driven I/O vs. Polling
The whole purpose of an interrupt driven system is to allow the CPU
to continue processing instructions while some I/O activity occurs. This
is in direct contrast to a polling system where the CPU continually tests
an I/O device to see if the I/O operation is complete. In an interrupt driven
system, the CPU goes about its business and the I/O device interrupts it
when it needs servicing. This is generally much more efficient than wasting
CPU cycles polling a device while it is not ready.
The serial port is a perfect example of a device that works extremely well
with interrupt driven I/O. You can start a communication program that begins
downloading a file over a modem. Each time a character arrives, it generates
an interrupt and the communication program starts up, buffers the character,
and then returns from the interrupt. In the meantime, another program (like
a word processor) can be running with almost no performance degradation
since it takes so little time to process the serial port interrupts.
Contrast the above scenario with one where the serial communication program
continually polls the serial communication chip to see if a character has
arrived. In this case the CPU spends all of its time looking for an input
character even though one rarely (in CPU terms) arrives. Therefore, no CPU
cycles are left over to do other processing like running your word processor.
Suppose interrupts were not available and you wanted to allow background
downloads while using your word processing program. Your word processing
program would have to test the input data on the serial port once every
few milliseconds to keep from losing any data. Can you imagine how difficult
such a word processor would be to write? An interrupt system is the clear
choice in this case.
If downloading data while word processing seems far fetched, consider a
more simple case - the PC's keyboard. Whenever a keypress interrupt occurs,
the keyboard ISR reads the key pressed and saves it in the system type ahead
buffer for the moment when the application wants to read the keyboard data.
Can you imagine how difficult it would be to write applications if you had
to constantly poll the keyboard port yourself to keep from losing characters?
Even in the middle of a long calculation? Once again, interrupts provide
an easy solution.
17.7.2 Interrupt Service Time
Of course, the serial communication system just described is an example
of a best case scenario. The communication program takes so little time
to do its job that most of the time is left over for the word processing
program. However, were to you run a different interrupt driven I/O system,
for example, copying files from one disk to another, the interrupt service
routine would have a noticeable impact on the performance of the word processing
system.
Two factors control an ISR's impact on a computer system: the frequency
of interrupts and the interrupt service time. The frequency is how many
times per second (or other time measurement) a particular interrupt occurs.
The interrupt service time is how long the ISR takes to service the interrupt.
The nature of the frequency varies according to source of the interrupt.
For example, the timer chip generates evenly spaced interrupts about 18
times per second, likewise, a serial port receiving at 9600bps generates
better than 100 interrupts per second. On the other hand, the keyboard rarely
generates more than about 20 interrupts per second and they are not very
regular.
The interrupt service time is obviously dependent upon the number of instructions
the ISR must execute. The interrupt service time is also dependent upon
the particular CPU and clock frequency. The same ISR executing identical
instructions on two CPUs will run in less time on a faster machine.
The amount of time an interrupt service routine takes to handle an interrupt,
multiplied by the frequency of the interrupt, determines the impact the
interrupt will have on system performance. Remember, every CPU cycle spent
in an ISR is one less cycle available for your application programs. Consider
the timer interrupt. Suppose the timer ISR takes 100 msec to complete
its tasks. This means that the timer interrupt consumes 1.8 msec out of
every second, or about 0.18% of the total computer time. Using a faster
CPU will reduce this percentage (by reducing the time spent in the ISR);
using a slower CPU will increase the percentage. Nevertheless, you can see
that a short ISR such as this one will not have a significant effect on
overall system performance.
One hundred microseconds is fast for a typical timer ISR, especially when
your system has several timer ISRs chained together. However, even if the
timer ISR took ten times as long to execute, it would only rob the system
of less than 2% of the available CPU cycles. Even if it took 100 times longer
(10 msec), there would only be an 18% performance degradation; most people
would barely notice such a degradation[9].
Of course, one cannot allow the ISR to take as much time as it wants. Since
the timer interrupt occurs every 55 msec, the maximum time the ISR can use
is just under 55msec. If the ISR requires more time than there is between
interrupts, the system will eventually lose an interrupt. Furthermore, the
system will spend all its time servicing the interrupt rather than accomplishing
anything else.
For many systems, having an ISR that consumes as much as 10% of the overall
CPU cycles will not prove to a problem. However, before you go off and start
designing slow interrupt service routines, you should remember that your
ISR is probably not the only ISR in the system. While your ISR is consuming
25% of the CPU cycles, there may be another ISR that is doing the same thing;
and another, and another, and... Furthermore, there may be some ISRs that
require fast servicing. For example, a serial port ISR may need to read
a character from the serial communications chip each millisecond or so.
If your timer ISR requires 4 msec to execute and does so with the interrupts
turned off, the serial port ISR will miss some characters.
Ultimately, of course, you would like to write ISRs so they are as fast
as possible so they have as little impact on system performance as they
can. This is one of the main reasons most ISRs for DOS are still written
in assembly language. Unless you are designing an embedded system, one in
which the PC runs only your application, you need to realize that your ISRs
must coexist with other ISRs and applications; you do not want the performance
of your ISR to adversely affect the performance of other code in the system.
17.7.3 Interrupt Latency
Interrupt latency is the time between the point a device signals that
it needs service and the point where the ISR provides the needed service.
This is not instantaneous! At the very least, the 8259 PIC needs to signal
the CPU, the CPU needs to interrupt the current program, push the flags
and return address, obtain the ISR address, and transfer control to the
ISR. The ISR may need to push various registers, set up certain variables,
check device status to determine the source of the interrupt, and so on.
Furthermore, there may be other ISRs chained into the interrupt vector before
you and they execute to completion before transferring control to your ISR
that actually services the device. Eventually, the ISR actually does whatever
it is that the device needs done. In the best case on the fastest microprocessors
with simple ISRs, the latency could be under a microsecond. On slower systems,
with several ISRs in a chain, the latency could be as bad as several milliseconds.
For some devices, the interrupt latency is more important than the actual
interrupt service time. For example, an input device may only interrupt
the CPU once every 10 seconds. However, that device may be incapable of
holding the data on its input port for more than a millisecond. In theory,
any interrupt service time less than 10 seconds is fine; but the CPU must
read the data within one millisecond of its arrival or the system will lose
the data.
Low interrupt latency (that is, responding quickly) is very important in
many applications. Indeed, in some applications the latency requirements
are so strict that you have to use a very fast CPU or you have to abandon
interrupts altogether and go back to polling. What a minute! Isn't polling
less efficient than an interrupt driven system? How will polling improve
things?
An interrupt driven I/O system improves system performance by allowing the
CPU to work on other tasks in between I/O operations. In principle, servicing
interrupts takes very little CPU time compared the arrival of interrupts
to the system. By using interrupt driven I/O, you can use all those other
CPU cycles for some other purpose. However, suppose the I/O device is producing
service requests at such a rate that there are no free CPU cycles. Interrupt
driven I/O will provide few benefits in this case.
For example, suppose we have an eight bit I/O device connected to two I/O
ports. Suppose bit zero of port 310h contains a one if data is available
and a zero otherwise. If data is available, the CPU must read the eight
bits at port 311h. Reading port 311h clears bit zero of port 310h until
the next byte arrives. If you wanted to read 8192 bytes from this port,
you could do this with the following short segment of code:
mov cx, 8192
mov dx, 310h
lea bx, Array ;Point bx at storage buffer
DataAvailLp: in al, dx ;Read status port.
shr al, 1 ;Test bit zero.
jnc DataAvailLp ;Wait until data is available.
inc dx ;Point at data port.
in al, dx ;Read data.
mov [bx], al ;Store data into buffer.
inc bx ;Move on to next array element.
dec dx ;Point back at status port.
loop DataAvailLp ;Repeat 8192 times.
.
.
.
This code uses a classical polling loop (DataAvailLp
) to wait
for each available character. Since there are only three instructions in
the polling loop, this loop can probably execute in just under a microsecond[10].
So it might take as much as one microsecond to determine that data is available,
in which case the code falls through and by the second instruction in the
sequence we've read the data from the device. Let's be generous and say
that takes another microsecond. Suppose, instead, we use a interrupt service
routine. A well-written ISR combined with a good system hardware design
will probably have latencies measured in microseconds.
To measure the best case latency we could hope to achieve would require
some sort of hardware timer than begins counting once an interrupt event
occurs. Upon entry into our interrupt service routine we could read this
counter to determine how much time has passed between the interrupt and
its service. Fortunately, just such a device exists on the PC - the 8254
timer chip that provides the source of the 55 msec interrupt.
The 8254 timer chip actually contains three separate timers: timer #0, timer
#1, and timer #2. The first timer (timer #0) provides the clock interrupt,
so it will be the focus of our discussion. The timer contains a 16 bit register
that the 8254 decrements at regular intervals (1,193,180 times per second).
Once the timer hits zero, it generates an interrupt on the 8259 IRQ 0 line
and then wraps around to 0FFFFh and continues counting down from that point.
Since the counter automatically resets to 0FFFFh after generating each interrupt,
this means that the 8254 timer generates interrupts every 65,536/1,193,180
seconds, or once every 54.9254932198 msec, which is 18.2064819336 times
per second. We'll just call these once every 55 msec or 18 (or 18.2) times
per second, respectively. Another way to view this is that the 8254 decrements
the counter once every 838 nanoseconds (or 0.838 msec).
The following short assembly language program measures interrupt latency
by patching into the int 8 vector. Whenever the timer chip counts down to
zero, it generates an interrupt that directly calls this program's ISR.
The ISR quickly reads the timer chip's counter register, negates the value
(so 0FFFFh becomes one, 0FFFEh becomes two, etc.), and then adds it to a
running total. The ISR also increments a counter so that it can keep track
of the number of times it has added a counter value to the total. Then the
ISR jumps to the original int 8 handler. The main program, in the mean time,
simply computes and displays the current average read from the counter.
When the user presses any key, this program terminates.
; This program measures the latency of an INT 08 ISR.
; It works by reading the timer chip immediately upon entering
; the INT 08 ISR By averaging this value for some number of
; executions, we can determine the average latency for this
; code.
.xlist
.386
option segment:use16
include stdlib.a
includelib stdlib.lib
.list
cseg segment para public 'code'
assume cs:cseg, ds:nothing
; All the variables are in the code segment in order to reduce ISR
; latency (we don't have to push and set up DS, saving a few instructions
; at the beginning of the ISR).
OldInt8 dword ?
SumLatency dword 0
Executions dword 0
Average dword 0
; This program reads the 8254 timer chip. This chip counts from
; 0FFFFh down to zero and then generates an interrupt. It wraps
; around from 0 to 0FFFFh and continues counting down once it
; generates the interrupt.
;
; 8254 Timer Chip port addresses:
Timer0_8254 equ 40h
Cntrl_8254 equ 43h
; The following ISR reads the 8254 timer chip, negates the result
; (because the timer counts backwards), adds the result to the
; SumLatency variable, and then increments the Executions variable
; that counts the number of times we execute this code. In the
; mean time, the main program is busy computing and displaying the
; average latency time for this ISR.
;
; To read the 16 bit 8254 counter value, this code needs to
; write a zero to the 8254 control port and then read the
; timer port twice (reads the L.O. then H.O. bytes). There
; needs to be a short delay between reading the two bytes
; from the same port address.
TimerISR proc near
push ax
mov eax, 0 ;Ch 0, latch & read data.
out Cntrl_8254, al ;Output to 8253 cmd register.
in al, Timer0_8254 ;Read latch #0 (LSB) & ignore.
mov ah, al
jmp SettleDelay ;Settling delay for 8254 chip.
SettleDelay: in al, Timer0_8254 ;Read latch #0 (MSB)
xchg ah, al
neg ax ;Fix, 'cause timer counts down.
add cseg:SumLatency, eax
inc cseg:Executions
pop ax
jmp cseg:OldInt8
TimerISR endp
Main proc
meminit
; Begin by patching in the address of our ISR into int 8's vector.
; Note that we must turn off the interrupts while actually patching
; the interrupt vector and we must ensure that interrupts are turned
; back on afterwards; hence the cli and sti instructions. These are
; required because a timer interrupt could come along between the two
; instructions that write to the int 8 interrupt vector. Since the
; interrupt vector is in an inconsistent state at that point, this
; could cause the system to crash.
mov ax, 0
mov es, ax
mov ax, es:[8*4]
mov word ptr OldInt8, ax
mov ax, es:[8*4 + 2]
mov word ptr OldInt8+2, ax
cli
mov word ptr es:[8*4], offset TimerISR
mov es:[8*4 + 2], cs
sti
; First, wait for the first call to the ISR above. Since we will be dividing
; by the value in the Executions variable, we need to make sure that it is
; greater than zero before we do anything.
Wait4Non0: cmp cseg:Executions, 0
je Wait4Non0
; Okay, start displaying the good values until the user presses a key at
; the keyboard to stop everything:
DisplayLp: mov eax, SumLatency
cdq ;Extends eax->edx.
div Executions
mov Average, eax
printf
byte "Count: %ld, average: %ld\n",0
dword Executions, Average
mov ah, 1 ;Test for keystroke.
int 16h
je DisplayLp
mov ah, 0 ;Read that keystroke.
int 16h
; Okay, restore the interrupt vector. We need the interrupts off
; here for the same reason as above.
mov ax, 0
mov es, ax
cli
mov ax, word ptr OldInt8
mov es:[8*4], ax
mov ax, word ptr OldInt8+2
mov es:[8*4+2], ax
sti
Quit: ExitPgm ;DOS macro to quit program.
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 1024 dup ("stack ")
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg ends
end Main
On a 66 MHz 80486 DX/2 processor, the above code reports an average value
of 44 after it has run for about 10,000 iterations. This works out to about
37 msec between the device signalling the interrupt and the ISR
being able to process it[11]. The latency of
polled I/O would probably be an order of magnitude less than this!
Generally, if you have some high speed application like audio or video recording
or playback, you probably cannot afford the latencies associated with interrupt
I/O. On the other hand, such applications demand such high performance out
of the system, that you probably wouldn't have any CPU cycles left over
to do other processing while waiting for I/O.
Another issue with respect to ISR latency is latency consistency. That is,
is there the same amount of latency from interrupt to interrupt? Some ISRs
can tolerate considerable latency as long as it is consistent (that is,
the latency is roughly the same from interrupt to interrupt). For example,
suppose you want to patch into the timer interrupt so you can read an input
port every 55 msec and store this data away. Later, when processing the
data, your code might work under the assumption that the data readings are
55 msec (or 54.9...) apart. This might not be true if there are other ISRs
in the timer interrupt chain before your ISR. For example, there may be
an ISR that counts off 18 interrupts and then executes some code sequence
that requires 10 msec. This means that 16 out of every 18 interrupts your
data collection routine would collect data at 55 msec intervals right on
the nose. But when that 18th interrupt occurs, the other timer ISR will
delay 10 msec before passing control to your routine. This means that your
17th reading will be 65 msec since the last reading. Don't forget, the timer
chip is still counting down during all of this, that means there are now
only 45 msec to the next interrupt. Therefore, your 18th reading would occur
45 msec after the 17th. Hardly a consistent pattern. If your ISR needs a
consistent latencies, you should try to install your ISR as early in the
interrupt chain as possible.
17.7.4 Prioritized Interrupts
Suppose you have the interrupts turned off for a brief spell (perhaps
you are processing some interrupt) and two interrupt requests come in while
the interrupts are off. What happens when you turn the interrupts back on?
Which interrupt will the CPU first service? The obvious answer would be
"whichever interrupt occurred first." However, suppose the both
occurred at exactly the same time (or, at least, within a short enough time
frame that we cannot determine which occurred first), or maybe, as is really
the case, the 8259 PIC cannot keep track of which interrupt occurred first?
Furthermore, what if one interrupt is more important that another? Suppose
for example, that one interrupt tells that the user has just pressed a key
on the keyboard and a second interrupt tells you that your nuclear reactor
is about to melt down if you don't do something in the next 100 msec.
Would you want to process the keystroke first, even if its interrupt came
in first? Probably not. Instead, you would want to prioritizes the interrupts
on the basis of their importance; the nuclear reactor interrupt is probably
a little more important than the keystroke interrupt, you should probably
handle it first.
The 8259 PIC provides several priority schemes, but the PC BIOS initializes
the 8259 to use fixed priority. When using fixed priorities, the device
on IRQ 0 (the timer) has the highest priority and the device on IRQ 7 has
the lowest priority. Therefore, the 8259 in the PC (running DOS) always
resolves conflicts in this manner. If you were going to hook that nuclear
reactor up to your PC, you'd probably want to use the nonmaskable interrupt
since it has a higher priority than anything provided by the 8259 (and you
can't mask it with a CLI instruction).
17.8 Debugging ISRs
Although writing ISRs can simplify the design of many types of programs,
ISRs are almost always very difficult to debug. There are two main reasons
ISRs are more difficult than standard applications to debug. First, as mentioned
earlier, errant ISRs can modify values the main program uses (or, worse
yet, that some other program in memory is using) and it is difficult to
pin down the source of the error. Second, most debuggers have fits when
you attempt to set breakpoints within an ISR.
If your code includes some ISRs and the program seems to be misbehaving
and you cannot immediately see the reason, you should immediately suspect
interference by the ISR. Many programmers have forgotten about ISRs appearing
in their code and have spent weeks attempting to locate a bug in their non-ISR
code, only to discover the problem was with the ISR. Always suspect the
ISR first. Generally, ISRs are short and you can quickly eliminate the ISR
as the cause of your problem before trying to track the bug down elsewhere.
Debuggers often have problems because they are not reentrant or they call
BIOS or DOS (that are not reentrant) so if you set a breakpoint in an ISR
that has interrupted BIOS or DOS and the debugger calls BIOS or DOS, the
system may crash because of the reentrancy problems. Fortunately, most modern
debuggers have a remote debugging mode that lets you connect a terminal
or another PC to a serial port and execute the debug commands on that second
display and keyboard. Since the debugger talks directly to the serial chip,
it avoids calling BIOS or DOS and avoids the reentrancy problems. Of course,
this doesn't help much if you're writing a serial ISR, but it works fine
with most other programs.
A big problem when debugging interrupt service routines is that the system
crashes immediately after you patch the interrupt vector. If you do not
have a remote debugging facility, the best approach to debug this code is
to strip the ISR to its bare essentials. This might be the code that simply
passes control on to the next ISR in the interrupt chain (if applicable).
Then add one section of code at a time back to your ISR until the ISR fails.
Of course, the best debugging strategy is to write code that doesn't have
any bugs. While this is not a practical solution, one thing you can do is
attempt to do as little as possible in the ISR. Simply read or write the
device's data and buffer any inputs for the main program to handle later.
The smaller your ISR is, the less complex it is, the higher the probability
is that it will not contain any bugs.
Debugging ISRs, unfortunately, is not easy and it is not something you can
learn right out of a book. It takes lots of experience and you will need
to make a lot of mistakes. There is unfortunately, but there is no substitute
for experience when debugging ISRs.
[8] There is no reason this has to be this
way, it's just that most people rarely run two programs at the same time
which must both be accessing the serial port.
[9]
As a general rule, people begin to notice a real difference in performance
between 25 and 50%. It isn't instantly obvious until about 50% (i.e., running
at one-half the speed).
[10] On a fast CPU
(.e.g, 100 MHz Pentium), you might expect this loop to execute in much less
time than one microsecond. However, the in
instruction is probably
going to be quite slow because of the wait states associated with external
I/O devices.
[11] Patching into the int 1Ch
interrupt vector produces latencies in the 137 msec range.
- 17.5 - Chaining Interrupt Service Routines
- 17.6 - Reentrancy Problems
- 17.7 - The Efficiency of an Interrupt
Driven System
- 17.7.1 - Interrupt Driven I/O vs. Polling
- 17.7.2 - Interrupt Service Time
- 17.7.3 - Interrupt Latency
- 17.7.4 - Prioritized Interrupts
- 17.8 - Debugging ISRs
Art of Assembly: Chaper Seventeen - 29 SEP 1996
[Chaper Seventeen][Previous]
[Art of Assembly][Randall
Hyde]